'use strict';

var testHelper = require('../support/test-helper');

var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/backends/template-maps');
var step = require('step');
var _ = require('underscore');

describe('template_maps', function () {
    // configure redis pool instance to use in tests
    var redisPool = new RedisPool(global.environment.redis);

    var keysToDelete;
    beforeEach(function () {
        keysToDelete = {};
    });
    afterEach(function (done) {
        testHelper.deleteRedisKeys(keysToDelete, done);
    });

    var wadusLayer = {
        options: {
            sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
            cartocss: '#layer { marker-fill:blue; }',
            cartocss_version: '2.3.0'
        }
    };

    var LAYER_PLAIN = {
        type: 'plain',
        options: {
            color: 'red'
        }
    };

    it('does not accept template with unsupported version', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var tpl = {
            version: '6.6.6',
            name: 'k',
            auth: {},
            layergroup: { layers: [wadusLayer] }
        };
        step(
            function () {
                tmap.addTemplate('me', tpl, this);
            },
            function checkFailed (err) {
                assert.ok(err);
                assert.ok(err.message.match(/unsupported.*version/i), err);
                return null;
            },
            function finish (err) {
                done(err);
            }
        );
    });

    it('does not accept template with missing name', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var tpl = {
            version: '0.0.1',
            auth: {},
            layergroup: { layers: [wadusLayer] }
        };
        step(
            function () {
                tmap.addTemplate('me', tpl, this);
            },
            function checkFailed (err) {
                assert.ok(err);
                assert.ok(err.message.match(/missing.*name/i), err);
                return null;
            },
            function finish (err) {
                done(err);
            }
        );
    });

    describe('naming', function () {
        function createTemplate (name) {
            return {
                version: '0.0.1',
                name: name,
                auth: {},
                layergroup: {
                    layers: [
                        wadusLayer
                    ]
                }
            };
        }
        var templateMaps = new TemplateMaps(redisPool);

        var invalidNames = ['ab|', 'a b', 'a@b', '-1ab', '_x', '', ' x', 'x '];
        invalidNames.forEach(function (invalidName) {
            it('should NOT accept template with invalid name: ' + invalidName, function (done) {
                templateMaps.addTemplate('me', createTemplate(invalidName), function (err) {
                    assert.ok(err, "Unexpected success with invalid name '" + invalidName + "'");
                    assert.ok(
                        err.message.match(/template.*name/i),
                        "Unexpected error message with invalid name '" + invalidName + "': " + err.message
                    );
                    done();
                });
            });
        });

        var validNames = [
            'AB', '1ab', 'DFD19A1A-0AC6-11E5-B0CA-6476BA93D4F6', '25ad8300-0ac7-11e5-b93f-6476ba93d4f6'
        ];
        validNames.forEach(function (validName) {
            it('should accept template with valid name: ' + validName, function (done) {
                templateMaps.addTemplate('me', createTemplate(validName), function (err) {
                    assert.ok(!err, "Unexpected error with valid name '" + validName + "': " + err);
                    keysToDelete['map_tpl|me'] = 0;
                    done();
                });
            });
        });
    });

    it('does not accept template with invalid placeholder name', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var tpl = {
            version: '0.0.1',
            name: 'valid',
            placeholders: {},
            auth: {},
            layergroup: { layers: [wadusLayer] }
        };
        var invalidnames = ['ab|', 'a b', 'a@b', '1ab', '_x', '', ' x', 'x '];
        var testNext = function () {
            if (!invalidnames.length) { done(); return; }
            var n = invalidnames.pop();
            tpl.placeholders = {};
            tpl.placeholders[n] = { type: 'number', default: 1 };
            tmap.addTemplate('me', tpl, function (err) {
                if (!err) {
                    done(new Error("Unexpected success with invalid name '" + n + "'"));
                } else if (!err.message.match(/invalid.*name/i)) {
                    done(new Error("Unexpected error message with invalid name '" + n + "': " + err));
                } else {
                    testNext();
                }
            });
        };
        testNext();
    });

    it('does not accept template with missing placeholder default', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var tpl = {
            version: '0.0.1',
            name: 'valid',
            placeholders: { v: {} },
            auth: {},
            layergroup: { layers: [wadusLayer] }
        };
        tmap.addTemplate('me', tpl, function (err) {
            if (!err) {
                done(new Error('Unexpected success with missing placeholder default'));
            } else if (!err.message.match(/missing default/i)) {
                done(new Error('Unexpected error message with missing placeholder default: ' + err));
            } else {
                done();
            }
        });
    });

    it('does not accept template with missing placeholder type', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var tpl = {
            version: '0.0.1',
            name: 'valid',
            placeholders: { v: { default: 1 } },
            auth: {},
            layergroup: { layers: [wadusLayer] }
        };
        tmap.addTemplate('me', tpl, function (err) {
            if (!err) {
                done(new Error('Unexpected success with missing placeholder type'));
            } else if (!err.message.match(/missing type/i)) {
                done(new Error('Unexpected error message with missing placeholder default: ' + err));
            } else {
                done();
            }
        });
    });

    // See http://github.com/CartoDB/Windshaft-cartodb/issues/128
    it('does not accept template with invalid token auth (undefined tokens)',
        function (done) {
            var tmap = new TemplateMaps(redisPool);
            assert.ok(tmap);
            var tpl = {
                version: '0.0.1',
                name: 'invalid_auth1',
                placeholders: { },
                auth: { method: 'token' },
                layergroup: { layers: [wadusLayer] }
            };
            tmap.addTemplate('me', tpl, function (err) {
                if (!err) {
                    done(new Error('Unexpected success with invalid token auth (undefined tokens)'));
                } else if (!err.message.match(/invalid 'token' authentication/i)) {
                    done(new Error('Unexpected error message with invalid token auth (undefined tokens): ' + err));
                } else {
                    done();
                }
            });
        });

    it('add, get and delete a valid template', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var expectedFailure = false;
        var tplId;
        var tpl = {
            version: '0.0.1',
            name: 'first',
            auth: {},
            layergroup: { layers: [wadusLayer] }
        };
        step(
            function () {
                tmap.addTemplate('me', tpl, this);
            },
            function addOmonimousTemplate (err, id) {
                assert.ifError(err);
                tplId = id;
                assert.strictEqual(tplId, 'first');
                expectedFailure = true;
                // should fail, as it already exists
                tmap.addTemplate('me', tpl, this);
            },
            function getTemplate (err) {
                if (!expectedFailure && err) {
                    throw err;
                }
                assert.ok(err);
                assert.ok(err.message.match(/already exists/i), err);
                tmap.getTemplate('me', tplId, this);
            },
            function delTemplate (err, gotTpl) {
                assert.ifError(err);
                assert.deepStrictEqual(gotTpl, _.extend({}, tpl, { auth: { method: 'open' }, placeholders: {} }));
                tmap.delTemplate('me', tplId, this);
            },
            function finish (err) {
                done(err);
            }
        );
    });

    it('add multiple templates, list them', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var tpl1 = { version: '0.0.1', name: 'first', auth: {}, layergroup: { layers: [wadusLayer] } };
        var tpl1Id;
        var tpl2 = { version: '0.0.1', name: 'second', auth: {}, layergroup: { layers: [wadusLayer] } };
        var tpl2Id;
        step(
            function addTemplate1 () {
                tmap.addTemplate('me', tpl1, this);
            },
            function addTemplate2 (err, id) {
                assert.ifError(err);
                tpl1Id = id;
                tmap.addTemplate('me', tpl2, this);
            },
            function listTemplates (err, id) {
                assert.ifError(err);
                tpl2Id = id;
                tmap.listTemplates('me', this);
            },
            function checkTemplates (err, ids) {
                assert.ifError(err);
                assert.strictEqual(ids.length, 2);
                assert.ok(ids.indexOf(tpl1Id) !== -1, ids.join(','));
                assert.ok(ids.indexOf(tpl2Id) !== -1, ids.join(','));
                return null;
            },
            function delTemplate1 (err) {
                if (tpl1Id) {
                    var next = this;
                    tmap.delTemplate('me', tpl1Id, function (e) {
                        if (err || e) {
                            next(new Error(err + '; ' + e));
                        } else {
                            next();
                        }
                    });
                } else {
                    assert.ifError(err);
                    return null;
                }
            },
            function delTemplate2 (err) {
                if (tpl2Id) {
                    var next = this;
                    tmap.delTemplate('me', tpl2Id, function (e) {
                        if (err || e) {
                            next(new Error(err + '; ' + e));
                        } else {
                            next();
                        }
                    });
                } else {
                    assert.ifError(err);
                    return null;
                }
            },
            function finish (err) {
                done(err);
            }
        );
    });

    it('update templates', function (done) {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);
        var expectedFailure = false;
        var owner = 'me';
        var tpl = {
            version: '0.0.1',
            name: 'first',
            auth: { method: 'open' },
            layergroup: { layers: [wadusLayer] }
        };
        var tplId;
        step(
            function addTemplate () {
                tmap.addTemplate(owner, tpl, this);
            },
            // Updating template name should fail
            function updateTemplateName (err, id) {
                assert.ifError(err);
                tplId = id;
                expectedFailure = true;
                tpl.name = 'second';
                tmap.updTemplate(owner, tplId, tpl, this);
            },
            function updateTemplateAuth (err) {
                if (err && !expectedFailure) {
                    throw err;
                }
                expectedFailure = false;
                assert.ok(err);
                tpl.name = 'first';
                tpl.auth.method = 'token';
                tpl.auth.valid_tokens = ['tok1'];
                tmap.updTemplate(owner, tplId, tpl, this);
            },
            function updateTemplateWithInvalid (err) {
                assert.ifError(err);
                tpl.version = '999.999.999';
                expectedFailure = true;
                tmap.updTemplate(owner, tplId, tpl, this);
            },
            function updateUnexistentTemplate (err) {
                if (err && !expectedFailure) {
                    throw err;
                }
                assert.ok(err);
                assert.ok(err.message.match(/unsupported.*version/i), err);
                tpl.version = '0.0.1';
                expectedFailure = true;
                tmap.updTemplate(owner, 'unexistent', tpl, this);
            },
            function delTemplate (err) {
                if (err && !expectedFailure) {
                    throw err;
                }
                expectedFailure = false;
                assert.ok(err);
                assert.ok(err.message.match(/cannot update name/i), err);
                tmap.delTemplate(owner, tplId, this);
            },
            function finish (err) {
                done(err);
            }
        );
    });

    it('instanciate templates', function () {
        var tmap = new TemplateMaps(redisPool);
        assert.ok(tmap);

        var tpl1 = {
            version: '0.0.1',
            name: 'acceptance1',
            auth: { method: 'open' },
            placeholders: {
                fill: { type: 'css_color', default: 'red' },
                color: { type: 'css_color', default: '#a0fF9A' },
                name: { type: 'sql_literal', default: 'test' },
                zoom: { type: 'number', default: '0' },
                test_number: { type: 'number', default: 23 }
            },
            layergroup: {
                version: '1.0.0',
                global_cartocss_version: '2.0.2',
                layers: [
                    {
                        options: {
                            sql: "select '<%=name %>' || id, g from t",
                            cartocss: '#layer { marker-fill:<%= fill %>; marker-width: <%=test_number  %>; }'
                        }
                    },
                    {
                        options: {
                            sql: "select fun('<%=     name%>') g from x",
                            cartocss: '#layer { line-color:<%= color %>; marker-fill:<%= color %>; }'
                        }
                    },
                    {
                        options: {
                            sql: 'select g from x',
                            cartocss: '#layer[zoom=<%=zoom%>] { }'
                        }
                    }
                ]
            }
        };

        var inst = tmap.instance(tpl1, {});

        var lyr = inst.layers[0].options;
        assert.strictEqual(lyr.sql, "select 'test' || id, g from t");
        assert.strictEqual(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');

        lyr = inst.layers[1].options;
        assert.strictEqual(lyr.sql, "select fun('test') g from x");
        assert.strictEqual(lyr.cartocss, '#layer { line-color:#a0fF9A; marker-fill:#a0fF9A; }');

        inst = tmap.instance(tpl1, { color: 'yellow', name: "it's dangerous" });

        lyr = inst.layers[0].options;
        assert.strictEqual(lyr.sql, "select 'it''s dangerous' || id, g from t");
        assert.strictEqual(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');

        lyr = inst.layers[1].options;
        assert.strictEqual(lyr.sql, "select fun('it''s dangerous') g from x");
        assert.strictEqual(lyr.cartocss, '#layer { line-color:yellow; marker-fill:yellow; }');

        // Invalid css_color
        var err = null;
        try { tmap.instance(tpl1, { color: '##ff00ff' }); } catch (e) { err = e; }
        assert.ok(err);
        assert.ok(err.message.match(/invalid css_color/i), err);

        // Invalid css_color 2 (too few digits)
        err = null;
        try { tmap.instance(tpl1, { color: '#ff' }); } catch (e) { err = e; }
        assert.ok(err);
        assert.ok(err.message.match(/invalid css_color/i), err);

        // Invalid css_color 3 (too many digits)
        err = null;
        try { tmap.instance(tpl1, { color: '#1234567' }); } catch (e) { err = e; }
        assert.ok(err);
        assert.ok(err.message.match(/invalid css_color/i), err);

        // Invalid number
        err = null;
        try { tmap.instance(tpl1, { zoom: '#' }); } catch (e) { err = e; }
        assert.ok(err);
        assert.ok(err.message.match(/invalid number/i), err);

        // Invalid number 2
        err = null;
        try { tmap.instance(tpl1, { zoom: '23e' }); } catch (e) { err = e; }
        assert.ok(err);
        assert.ok(err.message.match(/invalid number/i), err);

        // Valid number
        err = null;
        try { tmap.instance(tpl1, { zoom: '-.23e10' }); } catch (e) { err = e; }
        assert.ok(!err);
    });

    // Can set a limit on the number of user templates
    it('can limit number of user templates', function (done) {
        var tmap = new TemplateMaps(redisPool, {
            max_user_templates: 2
        });
        assert.ok(tmap);
        var tpl = { version: '0.0.1', auth: {}, layergroup: { layers: [wadusLayer] } };
        var expectErr = false;
        var idMe = [];
        var idYou = [];
        step(
            function oneForMe () {
                tpl.name = 'oneForMe';
                tmap.addTemplate('me', tpl, this);
            },
            function twoForMe (err, id) {
                assert.ifError(err);
                assert.ok(id);
                idMe.push(id);
                tpl.name = 'twoForMe';
                tmap.addTemplate('me', tpl, this);
            },
            function threeForMe (err, id) {
                assert.ifError(err);
                assert.ok(id);
                idMe.push(id);
                tpl.name = 'threeForMe';
                expectErr = true;
                tmap.addTemplate('me', tpl, this);
            },
            function errForMe (err/*, id */) {
                if (err && !expectErr) {
                    throw err;
                }
                expectErr = false;
                assert.ok(err);
                assert.ok(err.message.match(/limit.*template/), err);
                return null;
            },
            function delOneMe (err) {
                assert.ifError(err);
                tmap.delTemplate('me', idMe.shift(), this);
            },
            function threeForMeRetry (err) {
                assert.ifError(err);
                tpl.name = 'threeForMe';
                tmap.addTemplate('me', tpl, this);
            },
            function oneForYou (err, id) {
                assert.ifError(err);
                assert.ok(id);
                idMe.push(id);
                tpl.name = 'oneForYou';
                tmap.addTemplate('you', tpl, this);
            },
            function twoForYou (err, id) {
                assert.ifError(err);
                assert.ok(id);
                idYou.push(id);
                tpl.name = 'twoForYou';
                tmap.addTemplate('you', tpl, this);
            },
            function threeForYou (err, id) {
                assert.ifError(err);
                assert.ok(id);
                idYou.push(id);
                tpl.name = 'threeForYou';
                expectErr = true;
                tmap.addTemplate('you', tpl, this);
            },
            function errForYou (err/*, id */) {
                if (err && !expectErr) {
                    throw err;
                }
                expectErr = false;
                assert.ok(err);
                assert.ok(err.message.match(/limit.*template/), err);
                return null;
            },
            function finish (err) {
                keysToDelete['map_tpl|you'] = 0;
                keysToDelete['map_tpl|me'] = 0;
                // TODO: delete all templates
                done(err);
            }
        );
    });

    describe('emit', function () {
        var owner = 'me';
        var templateName = 'emit';
        var template = {
            version: '0.0.1',
            name: templateName,
            auth: {
                method: 'open'
            },
            placeholders: {},
            layergroup: {
                layers: [
                    wadusLayer
                ]
            }
        };
        var templateUpdated = _.extend({}, template, { layergroup: { layers: [LAYER_PLAIN] } });
        var templateMaps;
        beforeEach(function () {
            templateMaps = new TemplateMaps(redisPool);
        });

        it('should emit on template update', function (done) {
            templateMaps.on('update', function (_owner, _templateName, _template) {
                assert.strictEqual(_owner, owner);
                assert.strictEqual(_templateName, templateName);
                assert.deepStrictEqual(_template, templateUpdated);

                templateMaps.delTemplate(owner, templateName, done);
            });

            templateMaps.addTemplate(owner, template, function (err, templateName, _template) {
                assert.ok(!err, err);

                assert.deepStrictEqual(_template, template);
                templateMaps.updTemplate(owner, templateName, templateUpdated, function () {});
            });
        });

        it('should emit on template deletion', function (done) {
            templateMaps.on('delete', function (_owner, _templateName) {
                assert.strictEqual(_owner, owner);
                assert.strictEqual(_templateName, templateName);

                done();
            });

            templateMaps.addTemplate(owner, template, function (err, templateName, _template) {
                assert.ok(!err, err);

                assert.deepStrictEqual(_template, template);
                templateMaps.delTemplate(owner, templateName, function () {});
            });
        });

        it('should NOT emit on template update when template is the same', function (done) {
            templateMaps.on('update', function (_owner, _templateName, _template) {
                assert.strictEqual(_owner, owner);
                assert.strictEqual(_templateName, templateName);
                assert.deepStrictEqual(_template, templateUpdated);

                templateMaps.delTemplate(owner, templateName, done);
            });

            templateMaps.addTemplate(owner, template, function (err, templateName, _template) {
                assert.ok(!err, err);
                assert.deepStrictEqual(_template, template);

                templateMaps.updTemplate(owner, templateName, template, function () {
                    templateMaps.updTemplate(owner, templateName, templateUpdated, function () {});
                });
            });
        });
    });
});
